package objects

import (
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"jdfs2/internal/api/common"
	"jdfs2/internal/service"
	"jdfs2/internal/utils"
)

// RegisterRoutes registers object-related routes
func RegisterRoutes(api *gin.RouterGroup, s3svc service.S3Service) {
	// Object operations
	api.PUT("/:bucket/*key", putObject(s3svc))
	api.GET("/:bucket/*key", getObject(s3svc))
	api.DELETE("/:bucket/*key", deleteObject(s3svc))
	api.HEAD("/:bucket/*key", headObject(s3svc))
}

// putObject handles PUT /:bucket/*key (including upload part)
func putObject(s3svc service.S3Service) gin.HandlerFunc {
	return func(c *gin.Context) {
		bucket := c.Param("bucket")
		key := strings.TrimPrefix(c.Param("key"), "/")

		// CopyObject
		if src := c.Request.Header.Get("x-amz-copy-source"); src != "" && c.Query("uploadId") == "" {
			src = strings.TrimPrefix(src, "/")
			parts := strings.SplitN(src, "/", 2)
			if len(parts) != 2 {
				common.WriteS3Error(c, http.StatusBadRequest, "InvalidRequest", "Invalid x-amz-copy-source")
				return
			}
			etag, err := s3svc.CopyObject(c, bucket, key, parts[0], parts[1])
			if err != nil {
				if err == utils.ErrNoSuchKey || err == utils.ErrNoSuchBucket {
					common.WriteS3Error(c, http.StatusNotFound, "NoSuchKey", "The specified key does not exist.")
				} else {
					common.WriteS3Error(c, http.StatusInternalServerError, "InternalError", err.Error())
				}
				return
			}
			c.Header("Content-Type", "application/xml")
			_ = xml.NewEncoder(c.Writer).Encode(common.CopyObjectResult{
				ETag:         "\"" + etag + "\"",
				LastModified: time.Now().UTC().Format(time.RFC3339),
			})
			return
		}

		// Multipart Upload Part (including UploadPartCopy)
		if uploadID := c.Query("uploadId"); uploadID != "" {
			partNumStr := c.Query("partNumber")
			pn, _ := strconv.Atoi(partNumStr)

			if src := c.Request.Header.Get("x-amz-copy-source"); src != "" {
				src = strings.TrimPrefix(src, "/")
				parts := strings.SplitN(src, "/", 2)
				if len(parts) != 2 {
					common.WriteS3Error(c, http.StatusBadRequest, "InvalidRequest", "Invalid x-amz-copy-source")
					return
				}
				var start, end *int64
				if rng := c.Request.Header.Get("x-amz-copy-source-range"); rng != "" {
					// format: bytes=start-end
					if strings.HasPrefix(strings.ToLower(rng), "bytes=") {
						se := strings.TrimPrefix(strings.ToLower(rng), "bytes=")
						spl := strings.SplitN(se, "-", 2)
						if len(spl) == 2 {
							if s, err := strconv.ParseInt(strings.TrimSpace(spl[0]), 10, 64); err == nil {
								start = &s
							}
							if e, err := strconv.ParseInt(strings.TrimSpace(spl[1]), 10, 64); err == nil {
								end = &e
							}
						}
					}
				}
				etag, _, err := s3svc.UploadPartCopy(c, uploadID, pn, parts[0], parts[1], start, end)
				if err != nil {
					common.WriteS3Error(c, http.StatusNotFound, "NoSuchUpload", "The specified upload does not exist.")
					return
				}
				c.Header("ETag", "\""+etag+"\"")
				c.Status(http.StatusOK)
				return
			}

			// Get Content-MD5 header if present for part upload
			contentMD5 := c.GetHeader("Content-MD5")

			etag, _, err := s3svc.UploadPart(c, uploadID, pn, c.Request.Body, contentMD5)
			if err != nil {
				if s3Err, ok := err.(*utils.S3Error); ok {
					common.WriteS3Error(c, s3Err.HTTPStatusCode, s3Err.Code, s3Err.Message)
				} else {
					common.WriteS3Error(c, http.StatusNotFound, "NoSuchUpload", "The specified upload does not exist.")
				}
				return
			}
			c.Header("ETag", "\""+etag+"\"")
			c.Status(http.StatusOK)
			return
		}

		// Normal PutObject with metadata
		ct := c.GetHeader("Content-Type")
		if ct == "" {
			ct = "application/octet-stream"
		}
		userMeta := map[string]string{}
		for k, vs := range c.Request.Header {
			kl := strings.ToLower(k)
			if strings.HasPrefix(kl, "x-amz-meta-") && len(vs) > 0 {
				userMeta[kl[len("x-amz-meta-"):]] = vs[0]
			}
		}

		// Get Content-MD5 header if present
		contentMD5 := c.GetHeader("Content-MD5")

		etag, err := s3svc.PutObject(c, bucket, key, c.Request.Body, c.Request.ContentLength, ct, userMeta, contentMD5)
		if err != nil {
			if s3Err, ok := err.(*utils.S3Error); ok {
				common.WriteS3Error(c, s3Err.HTTPStatusCode, s3Err.Code, s3Err.Message)
			} else if err == utils.ErrNoSuchBucket {
				common.WriteS3Error(c, http.StatusNotFound, "NoSuchBucket", "The specified bucket does not exist.")
			} else {
				common.WriteS3Error(c, http.StatusInternalServerError, "InternalError", err.Error())
			}
			return
		}
		c.Header("ETag", "\""+etag+"\"")
		c.Status(http.StatusOK)
	}
}

// getObject handles GET /:bucket/*key
func getObject(s3svc service.S3Service) gin.HandlerFunc {
	return func(c *gin.Context) {
		bucket := c.Param("bucket")
		key := strings.TrimPrefix(c.Param("key"), "/")

		// ListParts (GET with uploadId)
		if uploadID := c.Query("uploadId"); uploadID != "" {
			partMarker, _ := strconv.Atoi(c.Query("part-number-marker"))
			maxParts, _ := strconv.Atoi(c.Query("max-parts"))
			parts, next, truncated, err := s3svc.ListParts(c, uploadID, partMarker, maxParts)
			if err != nil {
				common.WriteS3Error(c, http.StatusNotFound, "NoSuchUpload", "The specified upload does not exist.")
				return
			}
			var out common.ListPartsResult
			out.Bucket = bucket
			out.Key = key
			out.UploadID = uploadID
			out.PartNumberMarker = partMarker
			out.NextPartNumberMarker = next
			if maxParts <= 0 {
				maxParts = 1000
			}
			out.MaxParts = maxParts
			out.IsTruncated = truncated
			for _, p := range parts {
				out.Parts = append(out.Parts, common.PartSummary{
					PartNumber:   p.PartNumber,
					LastModified: p.LastModified,
					ETag:         "\"" + p.ETag + "\"",
					Size:         p.Size,
				})
			}
			c.Header("Content-Type", "application/xml")
			_ = xml.NewEncoder(c.Writer).Encode(out)
			return
		}

		file, meta, err := s3svc.GetObject(c, bucket, key)
		if err != nil {
			common.WriteS3Error(c, http.StatusNotFound, "NoSuchKey", "The specified key does not exist.")
			return
		}
		defer file.Close()

		// Conditional requests
		etagQuoted := "\"" + meta.ETag + "\""
		if inm := c.GetHeader("If-None-Match"); inm != "" && strings.Contains(inm, meta.ETag) {
			c.Status(http.StatusNotModified)
			return
		}
		if im := c.GetHeader("If-Match"); im != "" && !strings.Contains(im, meta.ETag) {
			c.Status(http.StatusPreconditionFailed)
			return
		}
		if ims := c.GetHeader("If-Modified-Since"); ims != "" {
			if t, err := http.ParseTime(ims); err == nil && !time.Unix(meta.LastModified, 0).After(t) {
				c.Status(http.StatusNotModified)
				return
			}
		}
		if ius := c.GetHeader("If-Unmodified-Since"); ius != "" {
			if t, err := http.ParseTime(ius); err == nil && time.Unix(meta.LastModified, 0).After(t) {
				c.Status(http.StatusPreconditionFailed)
				return
			}
		}

		// Range support
		size := meta.Size
		status := http.StatusOK
		var start, end int64 = 0, size - 1
		if rh := c.GetHeader("Range"); strings.HasPrefix(strings.ToLower(rh), "bytes=") && size > 0 {
			se := strings.TrimPrefix(strings.ToLower(rh), "bytes=")
			spl := strings.SplitN(se, "-", 2)
			if len(spl) == 2 {
				if spl[0] != "" {
					if s, err := strconv.ParseInt(strings.TrimSpace(spl[0]), 10, 64); err == nil {
						start = s
					}
				}
				if spl[1] != "" {
					if e, err := strconv.ParseInt(strings.TrimSpace(spl[1]), 10, 64); err == nil {
						end = e
					}
				}
				if start < 0 {
					start = 0
				}
				if end >= size {
					end = size - 1
				}
				if start <= end && start < size {
					status = http.StatusPartialContent
					if _, err := file.Seek(start, io.SeekStart); err == nil {
						c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
						c.Header("Content-Length", strconv.FormatInt(end-start+1, 10))
						c.Header("Accept-Ranges", "bytes")
						c.Header("ETag", etagQuoted)
						if meta.ContentType != "" {
							c.Header("Content-Type", meta.ContentType)
						} else {
							c.Header("Content-Type", "application/octet-stream")
						}
						c.Header("Last-Modified", time.Unix(meta.LastModified, 0).UTC().Format(time.RFC1123))
						c.Status(status)
						_, _ = io.CopyN(c.Writer, file, end-start+1)
						return
					}
				}
			}
		}

		// Full object
		c.Header("ETag", etagQuoted)
		if meta.ContentType != "" {
			c.Header("Content-Type", meta.ContentType)
		} else {
			c.Header("Content-Type", "application/octet-stream")
		}
		c.Header("Last-Modified", time.Unix(meta.LastModified, 0).UTC().Format(time.RFC1123))
		c.Status(status)
		if status == http.StatusOK {
			_, _ = io.Copy(c.Writer, file)
		}
	}
}

// deleteObject handles DELETE /:bucket/*key (including abort multipart)
func deleteObject(s3svc service.S3Service) gin.HandlerFunc {
	return func(c *gin.Context) {
		bucket := c.Param("bucket")
		key := strings.TrimPrefix(c.Param("key"), "/")

		// Abort multipart
		if uploadID := c.Query("uploadId"); uploadID != "" {
			if err := s3svc.AbortMultipart(c, uploadID); err != nil {
				common.WriteS3Error(c, http.StatusNotFound, "NoSuchUpload", "The specified upload does not exist.")
				return
			}
			c.Status(http.StatusNoContent)
			return
		}

		if err := s3svc.DeleteObject(c, bucket, key); err != nil {
			common.WriteS3Error(c, http.StatusNotFound, "NoSuchKey", "The specified key does not exist.")
			return
		}
		c.Status(http.StatusNoContent)
	}
}

// headObject handles HEAD /:bucket/*key
func headObject(s3svc service.S3Service) gin.HandlerFunc {
	return func(c *gin.Context) {
		bucket := c.Param("bucket")
		key := strings.TrimPrefix(c.Param("key"), "/")
		meta, err := s3svc.HeadObject(c, bucket, key)
		if err != nil {
			if err == utils.ErrNoSuchBucket {
				common.WriteS3Error(c, http.StatusNotFound, "NoSuchBucket", "The specified bucket does not exist.")
			} else {
				common.WriteS3Error(c, http.StatusNotFound, "NoSuchKey", "The specified key does not exist.")
			}
			return
		}
		c.Header("ETag", "\""+meta.ETag+"\"")
		c.Header("Content-Length", strconv.FormatInt(meta.Size, 10))
		if meta.ContentType != "" {
			c.Header("Content-Type", meta.ContentType)
		}
		c.Status(http.StatusOK)
	}
}
